package phoenixAAD.WifiFlashDrive;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Scanner;

import org.apache.http.util.EncodingUtils;

import android.os.Handler;




public class Protocol extends Thread
{
	// variables for handler
	public static final int ACTIVE_CONNECTED = 0;
	public static final int PASSIVE_CONNECTION_ACCEPTED = 1;
	public static final int TRANSFER_FINISHED = 2;
	public static final int EXCEPTION = 3;
	
	// local variables
	private final Socket commandSocket;
	private SocketCreator socketCreator;
	private DataOutputStream out;
	private BufferedReader  in;
	private boolean isRunning = true;
	private boolean loggedIn = false;
	private Handler toServer;
	private String username;
	private String password;
	private final String rootDirectory;
	private String currentDirectory="/";
	private DataType dataType = DataType.ASCII_NON_PRINT;
	private DataStructure structure = DataStructure.FILE;
	private TransferMode mode = TransferMode.STREAM;
	private String clientIp;
	
	private final String RECEI_PREFIX, SEND_PREFIX, ENCODING;
	private final FTP ftp;
	
	ArrayList<Transfer> transfers = new ArrayList<Transfer>();
	
	private int readTimeout = 5000;
	
	/**
	 * Commands that the server will accept
	 */
	private enum Command{
		USER, PASS, OPTS, PWD, CWD, TYPE, PASV, LIST, NOOP, DELE, MKD, RMD, RNFR, RNTO, RETR, STOR, MODE, STRU,PORT,QUIT
		//SYST, FEAT,SITE, 

	}
	
	
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	Protocol(Socket socket, Handler toServer, String rootDirectory, FTP ftp, String encoding) throws IOException
	{
		this.ftp = ftp;
		this.ENCODING = encoding;
		System.out.println(ENCODING);
		this.RECEI_PREFIX = ftp.getResources().getString(R.string.status_receive_prefix);
		this.SEND_PREFIX = ftp.getResources().getString(R.string.status_send_prefix);
		this.rootDirectory = rootDirectory;
		this.commandSocket = socket;
		in = new BufferedReader(new InputStreamReader(socket.getInputStream(), encoding));
		out = new DataOutputStream(socket.getOutputStream());
		this.toServer = toServer;
	}

	/**
	 * Communicates with the client <br>
	 * This method is called by thread.start() 
	 */
	public void run() {
		try{
			send(Response.WELCOME);
			String client_username="";
			String fromFilename="";
			commandSocket.setSoTimeout(readTimeout);
			while(isRunning)
			{
				
				// read command
				String incomming;
				try{
					incomming = in.readLine();
				}
				catch(SocketTimeoutException ste)
				{
					send(Response.CONNECTION_TIMEOUT);
					commandSocket.close();
					loggedIn = false;
					break;
				}
				if(incomming == null) // connection was closed on client side
						throw new IOException();
				System.out.println(incomming);
				
				Scanner scan = new Scanner(incomming);
				try{
					Command command = Command.valueOf(scan.next().toUpperCase());
					String parameter = "";
					if(scan.hasNext())
						parameter = scan.nextLine().trim();
					
					// send log, hiding the password
					if(command != Command.PASS)
						ftp.sendClientLog(RECEI_PREFIX + incomming);
					else
						ftp.sendClientLog(RECEI_PREFIX + command + " *****");
					
					// send receive commands
					if(command == Command.USER){
						client_username = incomming.substring(incomming.indexOf(" ")+1,incomming.length());
						send(Response.NEED_PW);					
					}else if(command == Command.PASS){
						String client_password = incomming.substring(incomming.indexOf(" ")+1,incomming.length());	
						if(password.equals(client_password) && username.equals(client_username)){
							loggedIn = true;
							send(Response.LOGIN_SUCCESSFUL);
						}else
							send(Response.INCORRECT_LOGIN);
					}
					else if(loggedIn)
					{
						switch(command)
						{
						case OPTS:
						{
							Scanner scanner = new Scanner(parameter);
							if(scanner.hasNext())
							{
								if(scanner.next().toLowerCase().equals("utf8") && scanner.hasNext())
									if(scanner.next().toLowerCase().equals("on"))
										if(ENCODING.equals(FTP.UTF))
										{
											send(Response.ENCODE_SUCCESS);
											break;
										}
							}
							send(Response.ENCODE_FAILED);
							break;
						}
						case PWD:
							send("257 \""+currentDirectory+"\" is current directory.");
							break;
						case CWD:
						{
							// move up, also breaks a security risk where user could go to any folder
							if(parameter.equals("..") || parameter.contains("/..") || parameter.contains("../"))
							{
								String[] folders = currentDirectory.split("/");
								currentDirectory = "";
								for(int i = 0; i < folders.length - 1; i++)
									currentDirectory += folders[i] + "/";
							}
							else if(parameter.charAt(0) == '/') // full path supplied
								currentDirectory = parameter;
							else // move to that folder from current position
								currentDirectory = currentDirectory + parameter;
							if(!currentDirectory.endsWith("/"))
								currentDirectory += "/";
							File file = new File(rootDirectory + currentDirectory);
							if(file.exists() && file.isDirectory()){
								send("250 \""+currentDirectory+ "\" is current directory.");
							}else{
								send("550 File not found.");
							}
							break;
						}
						case TYPE:
						{
							try {
								dataType = DataType.getType(parameter);
								send(Response.TYPE_SET + dataType.toString());
							} catch (NotImplementedException e) {
								send(e.getMessage());
							} catch (SyntaxException e) {
								send(e.getMessage());
							}
							break;
						}
						case MODE:
						{
							try {
								mode = TransferMode.getMode(parameter.charAt(0));
								send(Response.MODE_SET + dataType.toString());
							} catch (NotImplementedException e) {
								send(e.getMessage());
							} catch (SyntaxException e) {
								send(e.getMessage());
							}
							break;
						}
						case STRU:
						{
							try {
								structure = DataStructure.getStructure(parameter.charAt(0));
								send(Response.STRUCTURE_SET + dataType.toString());
							} catch (NotImplementedException e) {
								send(e.getMessage());
							} catch (SyntaxException e) {
								send(e.getMessage());
							}
							break;
						}
						case PASV:
						{
							socketCreator = new SocketCreator(this, WiFiFlashDriveActivity.PORT_NUMBER+1);
							socketCreator.start();
							send("227 Entering Passive Mode "+processIp(WiFiFlashDriveActivity.IP_ADDRESS)+","+processPort(WiFiFlashDriveActivity.PORT_NUMBER+1)); // port 50001				
							break;
						}
						case LIST:
						{

							if(socketCreator == null)
								send(Response.CONNECT_FAILED);
							else
							{
								try {
									socketCreator.join();
								} catch (InterruptedException e) {
									throw new RuntimeException(); 
								}
								File file =  new File(rootDirectory+currentDirectory);
								if(file.exists())
								{
									Transfer trans = new SendData(this, socketCreator.getSocket(), file, ftp, ENCODING);
									socketCreator = null; 
									send(Response.CONNECTION_ACCEPT);
									trans.start();
									transfers.add(trans);
								}
								else
									send(Response.FILE_NOT_FOUND);
							}
							break;
						}
						case NOOP:
							send(Response.STILL_ALIVE);
							break;
							
						case DELE:
						{
							File file = getFile(parameter);
							if(file == null)
								send(Response.BAD_ARGUMENT);
							else if(file.exists()){
								boolean deleted = deleteFolder(file);
								if(deleted)
									send("250 File deleted");
								else
									send("451 File was not successfully deleted.");
							}else{
								send("550 File not found.");
							}
							break;
						}
						case MKD:{
							File file = getFile(parameter);
							if(file == null)
								send(Response.BAD_ARGUMENT);
							else if(file.mkdir())
								send("250 Directory successfully created.");
							else
								send("550 Directory creation failed.");
							break;
						}
						case RMD:{
							File dir = getFile(parameter);
							if(dir == null)
								send(Response.BAD_ARGUMENT);
							else if(deleteDirectory(dir)){
								send("250 Directory was successfully removed.");
							}else{
								send("550 Directory removal failed.");
							}
							break;
						}
						case RNFR:{
							fromFilename = parameter;
							File file= getFile(parameter);
							if(file == null)
								send(Response.BAD_ARGUMENT);
							else if(file.exists())
								send("350 file was found.");
							else
								send("550 File not found.");
							break;
						}
						case RNTO:
						{
							File fromFile = getFile(fromFilename);
							File toFile = getFile(parameter);
							if(fromFile == null || toFile == null)
								send(Response.BAD_ARGUMENT);
							else
							{
								boolean renamed= fromFile.renameTo(toFile);
								if(renamed)
									send("250 File has been renamed.");
								else
									send("451 Error renaming file. Check permissions.");
							}
							break;
						}
						case RETR:
						{
							if(socketCreator == null)
								send(Response.CONNECT_FAILED);
							else{
								try {
									socketCreator.join();
								} catch (InterruptedException e) {
									throw new RuntimeException(); 
								}
								File file = getFile(parameter);
								if(file == null)
									send(Response.BAD_ARGUMENT);
								else if(file.exists())
								{
									Transfer trans = new SendData(this, socketCreator.getSocket(), file, dataType, mode, structure, ftp);
									send(Response.CONNECTION_ACCEPT);
									trans.start();
									transfers.add(trans);
								}
								else
									send(Response.FILE_NOT_FOUND);
								socketCreator = null;
							}
							break;
						}
						case STOR:
						{
							if(socketCreator == null)
								send(Response.CONNECT_FAILED);
							else{
								try {
									socketCreator.join();
								} catch (InterruptedException e) {
									throw new RuntimeException(); 
								}
								File file = getFile(parameter);
								if(file == null)
									send(Response.BAD_ARGUMENT);
								else
								{
									Transfer trans = new ReceiveData(this, socketCreator.getSocket(), file, dataType, mode, structure, ftp);
									send(Response.CONNECTION_ACCEPT);
									trans.start();
									transfers.add(trans);
									
									socketCreator = null;
								}
							}
							break;
						}
						case PORT:
							String hostPort = incomming.substring(incomming.indexOf(" ")+1, incomming.length());
							String temp = hostPort.substring(0, hostPort.lastIndexOf(","));
							int hostIpDivide = temp.lastIndexOf(",");
							String hostIp = hostPort.substring(0,hostIpDivide);
							hostIp = hostIp.replaceAll(",", ".");
							String portString = hostPort.substring(hostIpDivide+1, hostPort.length());
							int portNumber = processPortString(portString);
							
							try{
								socketCreator = new SocketCreator(this, hostIp,portNumber);
								socketCreator.start();
								socketCreator.join();
								clientIp = socketCreator.getSocket().getInetAddress().toString();
								clientIp = clientIp.substring(clientIp.indexOf("/")+1,clientIp.length());
								if(!clientIp.equals(hostIp)){
									send("510 IP mismatch.");
								}else
									send("200 Command PORT okay.");		
							}catch(Exception e){
								send("510 PORT command failed.");
							}
							break;
						case QUIT:
							try{
							commandSocket.close();
							loggedIn = false;
							}catch(IOException e){}
						}
					}
					else // user is calling a command while not logged in
					{
						send(Response.NOT_LOGGEDIN);
					}
				} catch(IllegalArgumentException e) // command was not in the enum Commands
				{
					ftp.sendClientLog(RECEI_PREFIX + incomming);
					send(Response.UNKNOWN_COMMAND);
				}
			}
		} catch(IOException e)
		{
			isRunning = false;
			toServer.obtainMessage(Server.PROTOCOL_END, this).sendToTarget();
			ftp.sendLog(ftp.getResources().getString(R.string.status_client_disconnected));
		}
		
	}
	
	/**
	 * Returns the file/folder of the specified parameter<br>
	 * This will NOT change the currentDirectory<br>
	 * 
	 */
	private File getFile(String parameter)
	{
		if(parameter.equals("") || parameter.equals("..") || parameter.contains("/..") || parameter.contains("../")) // if allowed, client can access any folder outside of root
		{
			return null;
		}
		if(parameter.startsWith("/"))
			return new File(rootDirectory + parameter);
		
		return new File(rootDirectory+currentDirectory+parameter);
		
	}

	/**
	 * Recursively deletes everything within a folder
	 */
	private boolean deleteFolder(File file)
	{
		if (file.isDirectory())
		{
			for(File newFile : file.listFiles())
				if(!deleteFolder(newFile))
					return false;
		}
		return file.delete();
	}
	
	public void transferFinished(int message, Transfer transfer)
	{
		try{
			switch(message)
			{
			case TRANSFER_FINISHED:
				send(Response.TRANSFER_SUCCESS);
				ftp.sendProgressStatus(100);
				break;
			case EXCEPTION:
				send(Response.TRANSMIT_ERROR); 
				if(transfer instanceof SendData)
					ftp.sendProgressStatus(-1);
				break;
			}
		}catch(IOException e)
		{}
		try {
			transfer.interrupt();
			transfer.join();
		} catch (InterruptedException e) {}
		
		transfers.remove(transfer);
	}
	
	
	/**
	 * Sends the specified string to client
	 */
	private void send(String s) throws IOException
	{
		ftp.sendServerLog(SEND_PREFIX + s);
		byte[] stringBytes = EncodingUtils.getAsciiBytes(s + Response.EOL);		
		out.write(stringBytes);
	}
	
	/**
	 * Kills this thread and closes the connection
	 */
	public boolean close()
	{
		isRunning = false;
		// TODO: kill this thread, send server going offline to client
		for(Transfer trans : transfers)
		{
			trans.close();
		}
		try {
			in.close();
			//isr.close();
			out.close();
			commandSocket.close();
		} catch (IOException e) {}
		catch (NullPointerException np){}
		
		return true;
	}
	
	public String processIp(String ipaddress){
		String processedIpAddress = "";
		processedIpAddress="("+ipaddress.replace(".",",");		
		return processedIpAddress;
	}
	
	
	public int processPortString(String portString){
		String port1binary = Integer.toBinaryString(Integer.parseInt(portString.substring(0, portString.indexOf(","))));
		if(port1binary.length()!=8){
			int num = 8-port1binary.length();
			for(int i = num; i>0; i--){
				port1binary="0"+port1binary;
			}
		}
		String port2binary = Integer.toBinaryString(Integer.parseInt(portString.substring(portString.indexOf(",")+1, portString.length())));
		if(port2binary.length()!=8){
			int num = 8-port2binary.length();
			for(int i = num; i>0; i--){
				port2binary = "0"+port2binary;
			}
		}
		return Integer.parseInt(port1binary+port2binary,2);
	}
	public  String processPort(int portNumber){
		String processedPortNumber="";
		processedPortNumber = ""+ Long.toBinaryString(portNumber);
		if(processedPortNumber.length()!=16){
			int num = 16-processedPortNumber.length();
			for(int i = num; i>0; i--){
				processedPortNumber = "0"+processedPortNumber;
			}
		}
		processedPortNumber = ""+Integer.parseInt((String) processedPortNumber.subSequence(0, 8),2)+","
								+Integer.parseInt((String) processedPortNumber.subSequence(8, processedPortNumber.length()),2)+")";
		return processedPortNumber;
	}

	
	public boolean deleteDirectory(File dir){
		if(dir.isDirectory()){
			String[] children = dir.list();
			for (int i=0; i<children.length; i++) {
	            boolean success = deleteDirectory(new File(dir, children[i]));
	            if (!success) {
	                return false;
	            }
	        }
		}else
			return false;
		return dir.delete();
	}
	
	public boolean isTransferring()
	{
		return transfers.size() > 0;
	}
}
